package org.redmagic.client.spring;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.redmagic.client.RedMagicConfiguration;
import org.redmagic.client.api.Location;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.TypedStringValue;


/**
 * THIS SHOULD EXTEND org.springframework.beans.factory.config.PlaceholderConfigurerSupport
 */
public class RedMagicPropertiesConfigurer implements BeanFactoryPostProcessor, BeanNameAware, BeanFactoryAware {
	
	private static final Log LOG = LogFactory.getLog(RedMagicPropertiesConfigurer.class);
	
	private static final String DEFAULT_REGEX = "\\$magic\\{.+\\}";
	
	private Map<String, List<PropertyValue>> propertiesPendingUpdate = new HashMap<String, List<PropertyValue>>();
	//private Multimap<String, PropertyValue> propertiesPendingUpdateMM;
	
	private RedMagicConfiguration redMagic;
	
	private BeanFactory beanFactory;

	private String beanName;
	
	private String configLocation;
	
	private Pattern pattern = Pattern.compile(DEFAULT_REGEX);
	
	@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
		
		redMagic = new RedMagicConfiguration(configLocation);
		
		String[] beanNames = beanFactory.getBeanDefinitionNames();
		for (String beanName : beanNames) {
			
			// Check that we're not parsing our own bean definition,
			// to avoid failing on unresolvable placeholders in properties file locations.
			if (!(beanName.equals(this.beanName) && beanFactory.equals(this.beanFactory))) {
				
				checkPropertyValues(beanFactory.getBeanDefinition(beanName));
			}
		}
		
		if (!propertiesPendingUpdate.isEmpty()) {
			
			// Try to find all of the properties that need to be updated
			Map<String, String> convertedValues = getConvertedValues();

			// Update the properties with their new values
			updatePropertyValues(convertedValues);
		}
	}
	
	// Check if the property values of a bean definition should be replaced
	private void checkPropertyValues(BeanDefinition beanDefinition) {
		
		MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();
		for (PropertyValue propertyValue : propertyValues.getPropertyValues()) {

			String decoratedValue = ((TypedStringValue) propertyValue.getValue()).getValue();
			
			if (pattern.matcher(decoratedValue).matches()) {
				String value = decoratedValue.substring(7, decoratedValue.length()-1);
				System.out.println("Locate property: " + value);
				List<PropertyValue> properties = propertiesPendingUpdate.get(value);
				if (properties == null) {
					properties = new ArrayList<PropertyValue>();
					propertiesPendingUpdate.put(value, properties);
				}
				properties.add(propertyValue);
			} else {
				System.out.println("Don't update property: " + propertyValue);
			}
		}
	}
	
	private Map<String, String> getConvertedValues() {
		
		Map<String, String> convertedValues = new HashMap<String, String>();
		Set<String> propertiesToUpdate = new HashSet<String>(propertiesPendingUpdate.keySet());

		// Loop until we find all of the properties to update or we run out of locations to check
		while (!propertiesToUpdate.isEmpty()) {
			
			Location location = redMagic.getNextLocation();
			
			if (location == null) {
				break;
			}
			
			Map<String, String> results = location.updateProperties(propertiesToUpdate);
			
			convertedValues.putAll(results);
			
			// Stop looking for the properties that we found
			for (String key : results.keySet()) {
				propertiesToUpdate.remove(key);
			}
		}

		return convertedValues;
	}
	
	private void updatePropertyValues(Map<String, String> convertedValues) {

		for (String convertedKey : convertedValues.keySet()) {

			String convertedValue = convertedValues.get(convertedKey);
			List<PropertyValue> propertiesToUpdate = propertiesPendingUpdate.remove(convertedKey);
			
			for (PropertyValue property : propertiesToUpdate) {
				property.setConvertedValue(convertedValue);
			}
		}

		if (!propertiesPendingUpdate.isEmpty()) {
			
			for (String property : propertiesPendingUpdate.keySet()) {
				
				LOG.error("Unable to locate property: " + property);
			}
			
			throw new RuntimeException("Unable to locate " + propertiesPendingUpdate.size() + " properties");
		}
	}
	
	/**
	 * Only necessary to check that we're not parsing our own bean definition,
	 * to avoid failing on unresolvable placeholders in properties file locations.
	 * The latter case can happen with placeholders for system properties in
	 * resource locations.
	 * @see #setLocations
	 * @see org.springframework.core.io.ResourceEditor
	 */
	public void setBeanName(String beanName) {
		this.beanName = beanName;
	}

	/**
	 * Only necessary to check that we're not parsing our own bean definition,
	 * to avoid failing on unresolvable placeholders in properties file locations.
	 * The latter case can happen with placeholders for system properties in
	 * resource locations.
	 * @see #setLocations
	 * @see org.springframework.core.io.ResourceEditor
	 */
	public void setBeanFactory(BeanFactory beanFactory) {
		this.beanFactory = beanFactory;
	}
	
	public void setConfigLocation(String configLocation) {
		this.configLocation = configLocation;
	}
	
	public void setRegex(String regex) {
		this.pattern = Pattern.compile(regex);
	}
	
	public void setRedMagicConfiguration(RedMagicConfiguration redMagic) {
		this.redMagic = redMagic;
	}
}
